package com.googlecode.curvedmetromaps.core.logic.localsearch.hillclimbing;

import java.util.Random;

import playn.core.PlayN;

import com.googlecode.curvedmetromaps.core.logic.costfunction.CostSystem;
import com.googlecode.curvedmetromaps.core.model.MetroMap;

public class HillClimbing {
	
	private final static int threads = 4;
	
	private MetroMap[] maps;
	private double[] costs;
	private MetroMap bestMap;
	private double change = 0.0001;
	private int targetIndex = 0;
	private int targetType = 0;
	
	private double currentBestCost = Double.POSITIVE_INFINITY;
	
	private Random random = new Random();
	private int generation = 0;
	
	public HillClimbing(MetroMap initialMap) {
		bestMap = initialMap;
		maps = new MetroMap[threads];
		costs = new double[threads];
		for (int i = 0; i < maps.length; i++) {
			maps[i] = new MetroMap();
		}
	}

	/**
	 * Increments map.
	 * @return The best new map.
	 */
	public MetroMap update() {
		double startTime = PlayN.currentTime();

		while (startTime + 100 > PlayN.currentTime()) {
			// Move to next target
			incrementChangeTarget();
			
			// Generate maps with a single points moved.
			generateMaps();
			
			// Calculate costs
			int minCostI = 0;
			double minCost = Double.POSITIVE_INFINITY;
			for (int i = 0; i < maps.length; i++) {
				MetroMap map = maps[i];
				CostSystem costSystem = new CostSystem(map);
				costs[i] = costSystem.calculateCostSum();
				if (costs[i] < minCost) {
					minCost = costs[i];
					minCostI = i;
				}
			}

			// Set bestMap to the map with the lowest cost if a better map is found.
			// Also increase the change if a better map was found and otherwise decrease it.
			if (minCost < currentBestCost) {
				currentBestCost = minCost;
				bestMap.copyFrom(maps[minCostI]);
				change *= 1.01;
			} else {
				change /= 1.0001;
			}
			
			if (generation % 100 == 0) {
				System.out.println(
					String.valueOf(generation) + "\t" +
					String.valueOf(currentBestCost) + "\t" +
					String.valueOf(change)
				);
			}

			generation++;
		}
		
		return bestMap;
	}

	private void generateMaps() {
		double angleStep = 2 * Math.PI / (double)threads;
		double rndAngle = random.nextDouble() * angleStep;
		int changeType = random.nextInt(2);
		
		for (int i = 0; i < maps.length; i++) {
			MetroMap map = maps[i];
			map.copyFrom(bestMap);

			double angle = rndAngle + i * angleStep;
			
			double dX = change * Math.cos(angle);
			double dY = change * Math.sin(angle);
			
			if (targetType == 0) {
				map.stations[targetIndex].x += dX;
				map.stations[targetIndex].x += dY;
			} else if (changeType == 1) {
				map.edgeArray[targetIndex].cpX += dX;
				map.edgeArray[targetIndex].cpY += dY;
			}
		}
	}
	
	private void incrementChangeTarget() {
		if (targetType == 0) {
			if (++targetIndex >= bestMap.stations.length) {
				targetIndex = 0;
				targetType = 1;
			}
		} else {
			if (++targetIndex >= bestMap.edgeArray.length) {
				targetIndex = 0;
				targetType = 0;
			}
		}
	}
	
	private void randomizeChangeTarget() {
		targetType = random.nextInt(2);
		if (targetType == 0) {
			targetIndex = random.nextInt(bestMap.stations.length);
		} else {
			targetIndex = random.nextInt(bestMap.edgeArray.length);
		}
	}
}
